Открытый курс по машинному обучению. Сессия № 2

</center> Автор материала: Илья Барышников. Материал распространяется на условиях лицензии Creative Commons CC BY-NC-SA 4.0. Можно использовать в любых целях (редактировать, поправлять и брать за основу), кроме коммерческих, но с обязательным упоминанием автора материала.

Домашнее задание №2

Визуальный анализ данных о сердечно-сосудистых заболеваниях

В задании предлагается с помощью визуального анализа ответить на несколько вопросов по данным о сердечно-сосудистых заболеваниях. Данные использовались в соревновании Ml Boot Camp 5 (качать их не надо, они уже есть в репозитории).

Заполните код в клетках (где написано "Ваш код здесь") и ответьте на вопросы в веб-форме.

В соревновании предлагалось определить наличие/отсутствие сердечно-сосудистых заболеваний (ССЗ) по результатам осмотра пациента.

Описание данных.

Объективные признаки:

  • Возраст (age)
  • Рост (height)
  • Вес (weight)
  • Пол (gender)

Результаты измерения:

  • Артериальное давление верхнее и нижнее (ap_hi, ap_lo)
  • Холестерин (cholesterol)
  • Глюкоза (gluc)

Субъективные признаки (со слов пациентов):

  • Курение (smoke)
  • Употребление алкоголя (alco)
  • Физическая активность (active)

Целевой признак (который интересно будет прогнозировать):

  • Наличие сердечно-сосудистых заболеваний по результатам классического врачебного осмотра (cardio)

Значения показателей холестерина и глюкозы представлены одним из трех классов: норма, выше нормы, значительно выше нормы. Значения субъективных признаков — бинарны.

Все показатели даны на момент осмотра.


In [2]:
# подгружаем все нужные пакеты
import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib
import matplotlib.pyplot as plt
import matplotlib.ticker
%matplotlib inline

# настройка внешнего вида графиков в seaborn
sns.set_context(
    "notebook", 
    font_scale = 1.5,       
    rc = { 
        "figure.figsize" : (12, 9), 
        "axes.titlesize" : 18 
    }
)

Проведем небольшой EDA


In [3]:
train = pd.read_csv('../../data/mlbootcamp5_train.csv', sep=';',
                    index_col='id')

In [4]:
print('Размер датасета: ', train.shape)
train.head()


Размер датасета:  (70000, 12)
Out[4]:
age gender height weight ap_hi ap_lo cholesterol gluc smoke alco active cardio
id
0 18393 2 168 62.0 110 80 1 1 0 0 1 0
1 20228 1 156 85.0 140 90 3 1 0 0 1 1
2 18857 1 165 64.0 130 70 3 1 0 0 0 1
3 17623 2 169 82.0 150 100 1 1 0 0 1 1
4 17474 1 156 56.0 100 60 1 1 0 0 0 0

Для начала всегда неплохо бы посмотреть на значения, которые принимают переменные.

Переведем данные в "Long Format"-представление и отрисуем с помощью factorplot количество значений, которые принимают категориальные переменные.


In [5]:
train_uniques = pd.melt(frame=train, value_vars=['gender','cholesterol', 
                                                 'gluc', 'smoke', 'alco', 
                                                 'active', 'cardio'])
train_uniques = pd.DataFrame(train_uniques.groupby(['variable', 
                                                    'value'])['value'].count()) \
    .sort_index(level=[0, 1]) \
    .rename(columns={'value': 'count'}) \
    .reset_index()
    
sns.factorplot(x='variable', y='count', hue='value', 
               data=train_uniques, kind='bar', size=12);


/usr/local/lib/python3.5/dist-packages/matplotlib/font_manager.py:1297: UserWarning: findfont: Font family ['sans-serif'] not found. Falling back to DejaVu Sans
  (prop.get_family(), self.defaultFamily[fontext]))

Видим, что классы целевой переменно сбалансированы, отлично!

Можно также разбить элементы обучающей выборки по значениям целевой переменной: иногда на таких графиках можно сразу увидеть самый значимый признак.


In [6]:
train_uniques = pd.melt(frame=train, value_vars=['gender','cholesterol', 
                                                 'gluc', 'smoke', 'alco', 
                                                 'active'], 
                        id_vars=['cardio'])
train_uniques = pd.DataFrame(train_uniques.groupby(['variable', 'value', 
                                                    'cardio'])['value'].count()) \
    .sort_index(level=[0, 1]) \
    .rename(columns={'value': 'count'}) \
    .reset_index()
    
sns.factorplot(x='variable', y='count', hue='value', 
               col='cardio', data=train_uniques, kind='bar', size=9);


/usr/local/lib/python3.5/dist-packages/matplotlib/font_manager.py:1297: UserWarning: findfont: Font family ['sans-serif'] not found. Falling back to DejaVu Sans
  (prop.get_family(), self.defaultFamily[fontext]))

Видим, что в зависимости от целевой переменной сильно меняется распределение холестерина и глюкозы. Совпадение?

Немного статистики по уникальным значениям признаков.


In [6]:
for c in train.columns:
    n = train[c].nunique()
    print(c)
    
    if n <= 3:
        print(n, sorted(train[c].value_counts().to_dict().items()))
    else:
        print(n)
    print(10 * '-')


age
8076
----------
gender
2 [(1, 45530), (2, 24470)]
----------
height
109
----------
weight
287
----------
ap_hi
153
----------
ap_lo
157
----------
cholesterol
3 [(1, 52385), (2, 9549), (3, 8066)]
----------
gluc
3 [(1, 59479), (2, 5190), (3, 5331)]
----------
smoke
2 [(0, 63831), (1, 6169)]
----------
alco
2 [(0, 66236), (1, 3764)]
----------
active
2 [(0, 13739), (1, 56261)]
----------
cardio
2 [(0, 35021), (1, 34979)]
----------

Итого:

  • Пять количественных признаков (без id)
  • Семь категориальных
  • 70000 элементов

1. Визуализируем корреляционную матрицу

Для того, чтобы лучше понять признаки в датасете, можно посчитать матрицу коэффициентов корреляции между признаками.

Постройте heatmap корреляционной матрицы. Матрица формируется средствами pandas, со стандартным значением параметров.

1. Какие два признака больше всего коррелируют (по Пирсону) с признаком gender ?

  • Cardio, Cholesterol
  • Height, Smoke
  • Smoke, Alco
  • Height, Weight

In [9]:
corr_matrix = train.drop(['age', 'ap_hi', 'ap_lo',
                      'gluc', 'active'], axis=1).corr()

sns.heatmap(corr_matrix, annot=True)


/usr/local/lib/python3.5/dist-packages/matplotlib/font_manager.py:1297: UserWarning: findfont: Font family ['sans-serif'] not found. Falling back to DejaVu Sans
  (prop.get_family(), self.defaultFamily[fontext]))
Out[9]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f24d3af5278>

2. Распределение роста для мужчин и женщин

Как мы увидели, в процессе исследования уникальных значений, пол кодируется значениями 1 и 2, расшифровка изначально не была нам дана в описании данных, но мы догадались, кто есть кто, посчитав средние значения роста (или веса) при разных значениях признака gender. Теперь сделаем то же самое, но графически.

Постройте violinplot для роста и пола. Используйте:

  • hue – для разбивки по полу
  • scale – для оценки количества каждого из полов

Для корректной отрисовки, преобразуйте DataFrame в "Long Format"-представление с помощью функции melt в pandas.
еще один пример

Постройте на одном графике два отдельных kdeplot роста, отдельно для мужчин и женщин. На нем разница будет более наглядной, но нельзя будет оценить количество мужчин/женщин.


In [8]:
train.head()


Out[8]:
age gender height weight ap_hi ap_lo cholesterol gluc smoke alco active cardio
id
0 18393 2 168 62.0 110 80 1 1 0 0 1 0
1 20228 1 156 85.0 140 90 3 1 0 0 1 1
2 18857 1 165 64.0 130 70 3 1 0 0 0 1
3 17623 2 169 82.0 150 100 1 1 0 0 1 1
4 17474 1 156 56.0 100 60 1 1 0 0 0 0

In [4]:
filtered_df = train[(train['ap_lo'] <= train['ap_hi']) & 
                 (train['height'] >= train['height'].quantile(0.025)) &
                 (train['height'] <= train['height'].quantile(0.975)) &
                 (train['weight'] >= train['weight'].quantile(0.025)) & 
                 (train['weight'] <= train['weight'].quantile(0.975))]

df = pd.melt(filtered_df, value_vars=['weight'], id_vars='gender')
sns.violinplot(x='variable', y='value', hue='gender', data=df)


Out[4]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f7d5716bef0>
/usr/local/lib/python3.5/dist-packages/matplotlib/font_manager.py:1297: UserWarning: findfont: Font family ['sans-serif'] not found. Falling back to DejaVu Sans
  (prop.get_family(), self.defaultFamily[fontext]))

In [6]:
female = filtered_df[filtered_df['gender'] == 1]
male = filtered_df[filtered_df['gender'] == 2]

ax = sns.kdeplot(female.height, female.height,
                 cmap="Reds", shade=True, shade_lowest=False)
ax = sns.kdeplot(male.height, male.height,
                cmap="Blues", shade=True, shade_lowest=False)


/usr/local/lib/python3.5/dist-packages/matplotlib/font_manager.py:1297: UserWarning: findfont: Font family ['sans-serif'] not found. Falling back to DejaVu Sans
  (prop.get_family(), self.defaultFamily[fontext]))

3. Ранговая корреляция

В большинстве случаев достаточно воспользоваться линейным коэффициентом корреляции Пирсона для выявления закономерностей в данных, но мы пойдем чуть дальше и используем ранговую корреляцию, которая поможет нам выявить пары, в которых меньший ранг из вариационного ряда одного признака всегда предшествует большему другого (или наоборот, в случае отрицательной корреляции).

Постройте корреляционную матрицу, используя коэффициент Спирмена

3.1 Какие признаки теперь больше всего коррелируют (по Спирмену) друг с другом?

  • Height, Weight
  • Age, Weight
  • Cholesterol, Gluc
  • Cardio, Cholesterol
  • Ap_hi, Ap_lo
  • Smoke, Alco

In [12]:
filtered_df = train[(train['ap_lo'] <= train['ap_hi']) & 
                 (train['height'] >= train['height'].quantile(0.025)) &
                 (train['height'] <= train['height'].quantile(0.975)) &
                 (train['weight'] >= train['weight'].quantile(0.025)) & 
                 (train['weight'] <= train['weight'].quantile(0.975))]

corr_matrix = filtered_df.drop(['gender', 'active'], axis=1).corr(method = 'spearman')

sns.heatmap(corr_matrix, annot=True)


/usr/local/lib/python3.5/dist-packages/matplotlib/font_manager.py:1297: UserWarning: findfont: Font family ['sans-serif'] not found. Falling back to DejaVu Sans
  (prop.get_family(), self.defaultFamily[fontext]))
Out[12]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f24d3d726d8>

3.2 Почему мы получили такое большое (относительно) значение ранговой корреляции у этих признаков?

  • Неточности в данных (ошибки при сборе данных)
  • Связь ошибочна, переменные никак не должны быть связаны друг с другом
  • Природа данных

In [11]:
mybins=np.logspace(0,np.log(100),100)
data = sns.load_dataset('tips')
g = sns.JointGrid('total_bill', 'tip', data,xlim=[1,100],ylim=[0.01,100])
g.plot_marginals(sns.distplot, hist=True, kde=True, color='blue',bins=mybins)
g.plot_joint(plt.scatter, color='black', edgecolor='black')
ax = g.ax_joint
ax.set_xscale('log')
ax.set_yscale('log')
g.ax_marg_x.set_xscale('log')
g.ax_marg_y.set_yscale('log')


/usr/local/lib/python3.5/dist-packages/matplotlib/font_manager.py:1297: UserWarning: findfont: Font family ['sans-serif'] not found. Falling back to DejaVu Sans
  (prop.get_family(), self.defaultFamily[fontext]))

4. Совместное распределение признаков

Постройте совместный график распределения jointplot двух наиболее коррелирующих между собой признаков (по Спирмену).

Кажется, наш график получился неинформативным из-за выбросов в значениях. Постройте тот же график, но с логарифмической шкалой.


In [3]:
g = sns.JointGrid('ap_lo', 'ap_hi', train,xlim=[1,100000],ylim=[0.01,100000])
g.plot_marginals(sns.distplot, hist=True, kde=True, color='blue')
g.plot_joint(plt.scatter, color='black', edgecolor='black')
ax = g.ax_joint
ax.set_xscale('log')
ax.set_yscale('log')
g.ax_marg_x.set_xscale('log')
g.ax_marg_y.set_yscale('log')

"""Сетка"""
g.ax_joint.grid(True) 

"""Преобразуем логарифмические значения на шкалах в реальные"""
#g.ax_joint.yaxis.set_major_formatter(matplotlib.ticker.FuncFormatter(lambda x, pos: str(x) if x == 0 else str(round(int(np.exp(x))))))
#g.ax_joint.xaxis.set_major_formatter(matplotlib.ticker.FuncFormatter(lambda x, pos: str(x) if x == 0 else str(round(int(np.exp(x))))))


/usr/local/lib/python3.5/dist-packages/matplotlib/font_manager.py:1297: UserWarning: findfont: Font family ['sans-serif'] not found. Falling back to DejaVu Sans
  (prop.get_family(), self.defaultFamily[fontext]))
Out[3]:
'Преобразуем логарифмические значения на шкалах в реальные'

In [12]:
clear_ds = train[
 (train['ap_lo'] > 0) 
    & (train['ap_hi'] > 0)
    & (train['ap_hi'] >= train['ap_hi'].quantile(0.025)) 
    & (train['ap_hi'] <= train['ap_hi'].quantile(0.975))
    & (train['ap_lo'] >= train['ap_lo'].quantile(0.025)) 
    & (train['ap_lo'] <= train['ap_lo'].quantile(0.975))
]
clear_ds['ap_lo_log'] = np.log1p(clear_ds['ap_lo'])
clear_ds['ap_hi_log'] = np.log1p(clear_ds['ap_hi'])

g = sns.jointplot("ap_lo_log", "ap_hi_log", data=clear_ds)

"""Сетка"""
g.ax_joint.grid(True) 

"""Преобразуем логарифмические значения на шкалах в реальные"""
g.ax_joint.yaxis.set_major_formatter(matplotlib.ticker.FuncFormatter(lambda x, pos: str(round(int(np.exp(x))))))
g.ax_joint.xaxis.set_major_formatter(matplotlib.ticker.FuncFormatter(lambda x, pos: str(round(int(np.exp(x))))))


/usr/local/lib/python3.5/dist-packages/ipykernel_launcher.py:10: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy
  # Remove the CWD from sys.path while we load stuff.
/usr/local/lib/python3.5/dist-packages/ipykernel_launcher.py:11: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy
  # This is added back by InteractiveShellApp.init_path()
/usr/local/lib/python3.5/dist-packages/matplotlib/font_manager.py:1297: UserWarning: findfont: Font family ['sans-serif'] not found. Falling back to DejaVu Sans
  (prop.get_family(), self.defaultFamily[fontext]))

4.1 Сколько четко выраженных кластеров получилось на совместном графике выбранных признаков, с логарифмической шкалой?

  • 1
  • 2
  • 3
  • больше трех

In [ ]:
# Ваш код здесь

5. Barplot

Посчитаем, сколько полных лет было респондентам на момент их занесения в базу.


In [5]:
train['age_years'] = (train['age'] // 365.25).astype(int)

Постройте Countplot, где на оси абсцисс будет отмечен возраст, на оси ординат – количество. Каждое значение возраста должно иметь два столбца, соответствующих количеству человек каждого класса cardio данного возраста.

5. В каком возрасте количество пациентов с ССЗ впервые становится больше, чем здоровых?

  • 44
  • 53
  • 64
  • 70

In [6]:
sns.countplot(x="age_years", hue="cardio", data=train)


Out[6]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f6b0bab4320>
/usr/local/lib/python3.5/dist-packages/matplotlib/font_manager.py:1297: UserWarning: findfont: Font family ['sans-serif'] not found. Falling back to DejaVu Sans
  (prop.get_family(), self.defaultFamily[fontext]))